egw_debug.js ➔ get_client_log   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
dl 0
loc 23
rs 9.2333
c 0
b 0
f 0
1
/**
2
 * EGroupware clientside API object
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage api
7
 * @link http://www.egroupware.org
8
 * @author Andreas Stöckel (as AT stylite.de)
9
 * @author Ralf Becker <[email protected]>
10
 * @version $Id$
11
 */
12
13
/*egw:uses
14
	egw_core;
15
*/
16
17
/**
18
 * Log debug messages to browser console and persistent html5 localStorage
19
 *
20
 * localStorage is limited by a clientside quota, so we need to deal with
21
 * situation that storing something in localStorage will throw an exception!
22
 *
23
 * @param {string} _app
24
 * @param {object} _wnd
25
 */
26
egw.extend('debug', egw.MODULE_GLOBAL, function(_app, _wnd)
27
{
28
	"use strict";
29
30
	/**
31
	 * DEBUGLEVEL specifies which messages are printed to the console.
32
	 * Decrease the value of EGW_DEBUGLEVEL to get less messages.
33
	 *
34
	 * @type Number
35
	 * 0 = off, no logging
36
	 * 1 = only "error"
37
	 * 2 = -- " -- plus "warning"
38
	 * 3 = -- " -- plus "info"
39
	 * 4 = -- " -- plus "log"
40
	 * 5 = -- " -- plus a stacktrace
41
	 */
42
	var DEBUGLEVEL = 3;
43
44
	/**
45
	 * Log-level for local storage and error-display in GUI
46
	 *
47
	 * @type Number
48
	 * 0 = off, no logging AND no global error-handler bound
49
	 * 1 = ... see above
50
	 */
51
	var LOCAL_LOG_LEVEL = 0;
52
	/**
53
	 * Number of log-entries stored on client, new errors overwrite old ones
54
	 *
55
	 * @type Number
56
	 */
57
	var MAX_LOGS = 200;
58
	/**
59
	 * Number of last old log entry = next one to overwrite
60
	 *
61
	 * @type String
62
	 */
63
	var LASTLOG = 'lastLog';
64
	/**
65
	 * Prefix for key of log-message, message number gets appended to it
66
	 *
67
	 * @type String
68
	 */
69
	var LOG_PREFIX = 'log_';
70
71
	/**
72
	 * Log to clientside html5 localStorage
73
	 *
74
	 * @param {String} _level "navigation", "log", "info", "warn", "error"
75
	 * @param {Array} _args arguments to egw.debug
76
	 * @param {string} _stack
77
	 * @returns {Boolean} false if localStorage is NOT supported, null if level requires no logging, true if logged
78
	 */
79
	function log_on_client(_level, _args, _stack)
80
	{
81
		if (!window.localStorage) return false;
82
83
		switch(_level)
84
		{
85
			case 'warn':
86
				if (LOCAL_LOG_LEVEL < 2) return null;
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
87
			case 'info':
88
				if (LOCAL_LOG_LEVEL < 3) return null;
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
89
			case 'log':
90
				if (LOCAL_LOG_LEVEL < 4) return null;
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
91
			default:
92
				if (!LOCAL_LOG_LEVEL) return null;
93
		}
94
		var data = {
95
			time: (new Date()).getTime(),
96
			level: _level,
97
			args: _args
98
		};
99
		// Add in a trace, if no navigation _level
100
		if (_level != 'navigation')
101
		{
102
			if (_stack)
103
			{
104
				data.stack = _stack;
105
			}
106
			else
107
			{
108
				// IE needs to throw the error to get a stack trace!
109
				try {
110
					throw new Error;
111
				}
112
				catch(error) {
113
					data.stack = error.stack;
114
				}
115
			}
116
		}
117
		if (typeof window.localStorage[LASTLOG] == 'undefined')
118
		{
119
			window.localStorage[LASTLOG] = 0;
120
		}
121
		// check if MAX_LOGS changed in code --> clear whole log
122
		if (window.localStorage[LASTLOG] > MAX_LOGS)
123
		{
124
			clear_client_log();
125
		}
126
		try {
127
			window.localStorage[LOG_PREFIX+window.localStorage[LASTLOG]] = JSON.stringify(data);
128
			window.localStorage[LASTLOG] = (1 + parseInt(window.localStorage[LASTLOG])) % MAX_LOGS;
129
		}
130
		catch(e) {
131
			switch (e.name)
132
			{
133
				case 'QuotaExceededError':	// storage quota is exceeded --> delete whole log
134
				case 'NS_ERROR_DOM_QUOTA_REACHED':	// FF-name
135
					clear_client_log();
136
					break;
137
138
				default:
139
					// one of the args is not JSON.stringify, because it contains circular references eg. an et2 widget
140
					for(var i=0; i < data.args.length; ++i)
141
					{
142
						try {
143
							JSON.stringify(data.args[i]);
144
						}
145
						catch(e) {
146
							// for Class we try removing _parent and _children attributes and try again to stringify
147
							if (data.args[i] instanceof Class)
148
							{
149
								data.args[i] = jQuery.extend({}, data.args[i]);
150
								delete data.args[i]._parent;
151
								delete data.args[i]._children;
152
								try {
153
									JSON.stringify(data.args[i]);
154
									continue;	// stringify worked --> check other arguments
155
								}
156
								catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
157
									// ignore error and remove whole argument
158
								}
159
							}
160
							// if above doesnt work, we remove the attribute
161
							data.args[i] = '** removed, circular reference **';
162
						}
163
					}
164
			}
165
			try {
166
				window.localStorage[LOG_PREFIX+window.localStorage[LASTLOG]] = JSON.stringify(data);
167
				window.localStorage[LASTLOG] = (1 + parseInt(window.localStorage[LASTLOG])) % MAX_LOGS;
168
			}
169
			catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
170
				// ignore error, if eg. localStorage exceeds quota on client
171
			}
172
		}
173
	}
174
175
	/**
176
	 * Get log from localStorage with oldest message first
177
	 *
178
	 * @returns {Array} of Object with values for attributes level, message, trace
179
	 */
180
	function get_client_log()
181
	{
182
		var logs = [];
183
184
		if (window.localStorage && typeof window.localStorage[LASTLOG] != 'undefined')
185
		{
186
			var lastlog = parseInt(window.localStorage[LASTLOG]);
187
			for(var i=lastlog; i < lastlog+MAX_LOGS; ++i)
188
			{
189
				var log = window.localStorage[LOG_PREFIX+(i%MAX_LOGS)];
190
				if (typeof log != 'undefined')
191
				{
192
					try {
193
						logs.push(JSON.parse(log));
194
					}
195
					catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
196
						// ignore not existing log entries
197
					}
198
				}
199
			}
200
		}
201
		return logs;
202
	}
203
204
	/**
205
	 * Clears whole client log
206
	 */
207
	function clear_client_log()
208
	{
209
		// Remove indicator icon
210
		jQuery('#topmenu_info_error').remove();
211
212
		if (!window.localStorage) return false;
213
214
		var max = MAX_LOGS;
215
		// check if we have more log entries then allowed, happens if MAX_LOGS get changed in code
216
		if (window.localStorage[LASTLOG] > MAX_LOGS)
217
		{
218
			max = 1000;	// this should NOT be changed, if MAX_LOGS get's smaller!
219
		}
220
		for(var i=0; i < max; ++i)
221
		{
222
			if (typeof window.localStorage[LOG_PREFIX+i] != 'undefined')
223
			{
224
				delete window.localStorage[LOG_PREFIX+i];
225
			}
226
		}
227
		delete window.localStorage[LASTLOG];
228
229
		return true;
230
	}
231
232
	/**
233
	 * Format one log message for display
234
	 *
235
	 * @param {Object} log {{level: string, time: number, stack: string, args: array[]}} Log information
236
	 *	Actual message is in args[0]
237
	 * @returns {DOMNode}
238
	 */
239
	function format_message(log)
240
	{
241
		var row = document.createElement('tr');
242
		row.setAttribute('class', log.level);
243
		var timestamp = row.insertCell(-1);
244
		timestamp.appendChild(document.createTextNode(new Date(log.time)));
245
		timestamp.setAttribute('class', 'timestamp');
246
247
		var level = row.insertCell(-1);
248
		level.appendChild(document.createTextNode(log.level));
249
		level.setAttribute('class', 'level');
250
251
		var message = row.insertCell(-1);
252
		for(var i = 0; i < log.args.length; i++)
253
		{
254
255
			var arg = document.createElement('p');
256
			arg.appendChild(
257
				document.createTextNode(typeof log.args[i] == 'string' ? log.args[i] : JSON.stringify( log.args[i]))
258
			);
259
			message.appendChild(arg);
260
		}
261
262
		var stack = row.insertCell(-1);
263
		stack.appendChild(document.createTextNode(log.stack||''));
264
		stack.setAttribute('class','stack');
265
266
		return row;
267
	}
268
269
	/**
270
	 * Show user an error happend by displaying a clickable icon with tooltip of current error
271
	 */
272
	function raise_error()
273
	{
274
		var icon = jQuery('#topmenu_info_error');
275
		if (!icon.length)
276
		{
277
			var icon = jQuery(egw(_wnd).image_element(egw.image('dialog_error')));
278
			icon.addClass('topmenu_info_item').attr('id', 'topmenu_info_error');
279
			// ToDo: tooltip
280
			icon.on('click', egw(_wnd).show_log);
281
			jQuery('#egw_fw_topmenu_info_items,#topmenu_info').append(icon);
282
		}
283
	}
284
285
	// bind to global error handler, only if LOCAL_LOG_LEVEL > 0
286
	if (LOCAL_LOG_LEVEL)
287
	{
288
		jQuery(_wnd).on('error', function(e)
289
		{
290
			// originalEvent does NOT always exist in IE
291
			var event = typeof e.originalEvent == 'object' ? e.originalEvent : e;
292
			// IE(11) gives a syntaxerror on each pageload pointing to first line of html page (doctype).
293
			// As I cant figure out what's wrong there, we are ignoring it for now.
294
			if (navigator.userAgent.match(/Trident/i) && typeof event.name == 'undefined' &&
295
				Object.prototype.toString.call(event) == '[object ErrorEvent]' &&
296
				event.lineno == 1 && event.filename.indexOf('/index.php') != -1)
297
			{
298
				return false;
299
			}
300
			log_on_client('error', [event.message], typeof event.stack != 'undefined' ? event.stack : null);
301
			raise_error();
302
			// rethrow error to let browser log and show it in usual way too
303
			if (typeof event.error == 'object')
304
			{
305
				throw event.error;
306
			}
307
			throw event.message;
308
		});
309
	}
310
311
	/**
312
	 * The debug function can be used to send a debug message to the
313
	 * java script console. The first parameter specifies the debug
314
	 * level, all other parameters are passed to the corresponding
315
	 * console function.
316
	 */
317
	return {
318
		debug_level: function() {
319
			return DEBUGLEVEL;
320
		},
321
		debug: function(_level) {
322
			if (typeof _wnd.console != "undefined")
323
			{
324
				// Get the passed parameters and remove the first entry
325
				var args = [];
326
				for (var i = 1; i < arguments.length; i++)
327
				{
328
					args.push(arguments[i]);
329
				}
330
331
				// Add in a trace
332
				if (DEBUGLEVEL >= 5 && typeof (new Error).stack != "undefined")
333
				{
334
					var stack = (new Error).stack;
335
					args.push(stack);
336
				}
337
338
				if (_level == "log" && DEBUGLEVEL >= 4 &&
339
					typeof _wnd.console.log == "function")
340
				{
341
					_wnd.console.log.apply(_wnd.console, args);
342
				}
343
344
				if (_level == "info" && DEBUGLEVEL >= 3 &&
345
					typeof _wnd.console.info == "function")
346
				{
347
					_wnd.console.info.apply(_wnd.console, args);
348
				}
349
350
				if (_level == "warn" && DEBUGLEVEL >= 2 &&
351
					typeof _wnd.console.warn == "function")
352
				{
353
					_wnd.console.warn.apply(_wnd.console, args);
354
				}
355
356
				if (_level == "error" && DEBUGLEVEL >= 1 &&
357
					typeof _wnd.console.error == "function")
358
				{
359
					_wnd.console.error.apply(_wnd.console, args);
360
				}
361
			}
362
			// raise errors to user, if LOCAL_LOG_LEVEL > 0
363
			if (LOCAL_LOG_LEVEL && _level == "error") raise_error(args);
0 ignored issues
show
Bug introduced by
The call to raise_error seems to have too many arguments starting with args.
Loading history...
Bug introduced by
The variable args does not seem to be initialized in case typeof _wnd.console != "undefined" on line 322 is false. Are you sure the function raise_error handles undefined variables?
Loading history...
364
365
			// log to html5 localStorage
366
			if (typeof stack != 'undefined') args.pop();	// remove stacktrace again
367
			log_on_client(_level, args);
368
		},
369
370
		/**
371
		 * Display log to user because he clicked on icon showed by raise_error
372
		 *
373
		 * @returns {undefined}
374
		 */
375
		show_log: function()
376
		{
377
			var table = document.createElement('table');
378
			var body = document.createElement('tbody');
379
			var client_log = get_client_log();
380
			for(var i = 0; i < client_log.length; i++)
381
			{
382
				body.appendChild(format_message(client_log[i]));
383
			}
384
			table.appendChild(body);
385
386
			// Use a wrapper div for ease of styling
387
			var wrapper = document.createElement('div');
388
			wrapper.setAttribute('class', 'client_error_log');
389
			wrapper.appendChild(table);
390
391
			if(window.jQuery && window.jQuery.ui.dialog)
392
			{
393
				var $wrapper = jQuery(wrapper);
394
				// Start hidden
395
				jQuery('tr',$wrapper).addClass('hidden')
396
					.on('click', function() {
397
						jQuery(this).toggleClass('hidden',{});
398
						jQuery(this).find('.stack').children().toggleClass('ui-icon ui-icon-circle-plus');
399
					});
400
				// Wrap in div so we can control height
401
				jQuery('td',$wrapper).wrapInner('<div/>')
402
					.filter('.stack').children().addClass('ui-icon ui-icon-circle-plus');
403
404
				$wrapper.dialog({
405
					title: egw.lang('Error log'),
406
					buttons: [
407
						{text: egw.lang('OK'), click: function() {jQuery(this).dialog( "close" ); }},
408
						{text: egw.lang('clear'), click: function() {clear_client_log(); jQuery(this).empty();}}
409
					],
410
					width: 800,
411
					height: 400
412
				});
413
				$wrapper[0].scrollTop = $wrapper[0].scrollHeight;
414
			}
415
			if (_wnd.console) _wnd.console.log(get_client_log());
416
		}
417
	};
418
});
419
420